/*
 *  SSChartView.m
 *  MESLib
 * 
 * Copyright 2003-2009 Starline Software Ltd
 * 
 * This file is part of MESLib.
 * 
 * MESLib is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * MESLib is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License
 * along with MESLib. If not, see <http://www.gnu.org/licenses/>.
 */

//  Created by Tobias Haughton on Wed Jan 22 2003.


//SSChartView is the main view of the Catalyst document it displays and coordinates the documents events

#import "SSChartView.h"
#import "SSEventView.h"
#import "SSEventObject.h"
#import "SSCatalystDocument.h"

@implementation SSChartView

- (id)initWithFrame:(NSRect)frame {
    self = [super initWithFrame:frame];
    if (self) {
       NSNotificationCenter *nc  = [NSNotificationCenter defaultCenter];

       [nc addObserver:self
              selector:@selector(updateAfterFrameChange)
                  name:@"NSViewFrameDidChangeNotification"
                object:self];
       //register get a notification when our frame changes
       [nc addObserver:self
              selector:@selector(recalculateEBPositions)
                  name:@"SSUpdateEBDisplay"
                object:nil];
       [nc addObserver:self
              selector:@selector(prepareForReorderingFromActor:)
                  name:@"SSPrepareForReorderingFromActor"
                object:nil];

       selectionView = [[[SSSelectionView alloc] initWithFrame:NSMakeRect(0, 0, NSWidth([self frame]), NSHeight([self frame]))] autorelease];
       [self addSubview:selectionView];

       temporaryLink = [[NSBezierPath alloc] init];
       eventViewArray = [[NSMutableArray alloc] init];
       commentViewArray = [[NSMutableArray alloc] init];

       selectedObjects = [[NSMutableArray alloc] init];


       overlapUnit = SSDayUnit;
       overlapPixelsPerUnit = 10.0;

       
       overlapDisplayGenerator = [[SSOverlapDisplayGenerator alloc] init];
       //initilize the overlap display dict
       [self updateOverlapDisplayData];

       topIndexForRecalculation = -5;

       [self setDisplayMode:SSMatrixDisplayMode];

       

       NSImage *handCursorImage = [[NSImage alloc] initWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"handCursor" ofType:@"tiff"]];
       handCursor = [[NSCursor alloc] initWithImage:handCursorImage hotSpot:NSMakePoint(8, 1)];
    }
    return self;
}

- (void)awakeFromNib
{
   if([self enclosingScrollView]) {
      [[NSNotificationCenter defaultCenter] addObserver:self
                                               selector:@selector(viewScrolled)
                                                   name:@"NSViewBoundsDidChangeNotification"
                                                 object:[[self enclosingScrollView] contentView]];
   }
}

- (void)drawRect:(NSRect)rect {
   int i = 0;

   //if our height is not equal to the table make it so
   if(NSHeight([self frame]) != NSHeight([theTable frame])) {
      if(NSHeight([theTable frame]) > NSHeight([self visibleRect])) {
         //we only want to set this is it is bigger than the visible rect
         [self setFrameSize:NSMakeSize(NSWidth([self frame]), NSHeight([theTable frame]))];
         [self setNeedsDisplayInRect:[self visibleRect]];
      }
   }
   
   [[NSColor whiteColor] set];
   [NSBezierPath fillRect:[self bounds]];
   [[NSColor blackColor] set];

   [temporaryLink stroke]; //draw the temporaryLink

   while (i < [eventViewArray count]) {
      [[[eventViewArray objectAtIndex:i] owner] strokeLinksForVisibleLinkedTo]; //get the objects to draw their links

      if([self displayMode] == SSMatrixDisplayMode) {
         [[[eventViewArray objectAtIndex:i] owner] drawDiamonds];
      }

      i++;
   }

   if([self displayMode] == SSOverlapDisplayMode) {
      //we are in overlap
      if(!NSContainsRect([[overlapDisplayData objectForKey:@"rectValue"] rectValue], rect)) {
         //the rect in overlapDisplayData doesn't cover the current drawing area
         [self updateOverlapDisplayData];
      }
      
      [[NSGraphicsContext currentContext] setShouldAntialias:NO];
      [[NSColor colorWithCalibratedRed:0 green:0 blue:0 alpha:.2] set];
      [[overlapDisplayData objectForKey:@"unitPath"] stroke];
      [[NSColor blackColor] set];
      [[overlapDisplayData objectForKey:@"secondaryPath"] stroke];
      [[NSGraphicsContext currentContext] setShouldAntialias:YES];
   }
   
   
   [[theDocument jumpMapView] setNeedsDisplay:YES];

   [(SSDateDisplayView *)dateDisplayView display];
}

//*************************
- (void)updateOverlapDisplayData
{
   //update the display data for our view
   [overlapDisplayData release];
   overlapDisplayData = [[overlapDisplayGenerator displayDictionaryForRect:NSMakeRect((NSMinX([self visibleRect]) - 200), 0, (NSWidth([self visibleRect]) + 200), NSHeight([self bounds]))
                                                             rectStartDate:[self dateForXPosition:(NSMinX([self visibleRect]) - 200) isEstimated:NO]
                                                                      unit:[self overlapUnit]
                                                             pixelsPerUnit:[self overlapPixelsPerUnit]
                                                            textAttributes:[NSDictionary dictionaryWithObjectsAndKeys:[NSFont fontWithName:@"Helvetica" size:10],
                                                               NSFontAttributeName, nil, nil]] retain];
}

- (NSMutableDictionary *)overlapDisplayData
{
   return overlapDisplayData;
}

//*************************
- (void)addEventView:(SSEventView *)view
{
   if(![[self subviews] containsObject:view]) {
      NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
      [nc addObserver:self
             selector:@selector(recalculateEBPositionsForViewAndBelow:)
                 name:@"SSActorChangedForView"
               object:view];


      //set the fram origin to reflect the position in the tableview
      int actorIndex = [[theDocument filteredActorArray] indexOfObject:[[view owner] actor]];
      float rowHeight = [theDocument tableViewRowHeight];

      float yPos = ((rowHeight * actorIndex) + SSFirstEBYPosition);

      NSPoint origin = NSMakePoint(NSMinX([view frame]), yPos);
      [view setFrameOrigin:origin];

      [eventViewArray addObject:view];
      [self addSubview:view positioned:(NSWindowOrderingMode)NSWindowBelow relativeTo:[[self subviews] objectAtIndex:0]];
      [view setDisplayMode:[self displayMode]];
      
      //add a tracking cell to the dateDisplayView for this event
      [dateDisplayView addTrackingCellForEvent:[view owner]];

      //add this view to the jump map
      [[theDocument jumpMapView] addJumpMapRepForView:view];
   }
}

- (void)addCommentView:(SSCommentView *)view
{
   if(![[self subviews] containsObject:view]) {
      [commentViewArray addObject:view];
      [self addSubview:view positioned:(NSWindowOrderingMode)NSWindowAbove relativeTo:[[self subviews] lastObject]];

      //add this view to the jump map
      [[theDocument jumpMapView] addJumpMapRepForView:view];
   }
}

- (void)addJumpMapView:(SSJumpMapView *)view
{
   if(![[self subviews] containsObject:view] && [view isKindOfClass:[SSJumpMapView class]]) {
      //we are not currently displaying the jump map
      [self addSubview:view positioned:(NSWindowOrderingMode)NSWindowBelow relativeTo:selectionView];
      //set the jump map size so that it is correct for our current scale
      [view setFrameSize:NSMakeSize((SSJumpMapSize.width * [self convertSize:NSMakeSize(1,1) fromView:nil].width), (SSJumpMapSize.height * [self convertSize:NSMakeSize(1,1) fromView:nil].height))];
      //set the jump maps position
      [[theDocument jumpMapView] setFrameOrigin:NSMakePoint((NSMaxX([self visibleRect]) - NSWidth([view frame])), (NSMaxY([self visibleRect]) - NSHeight([view frame])))];
      [view setRatios];
      [view setNavRecOrigin];
      displayingJumpMap = YES;
   }
}

- (void)willRemoveSubview:(NSView *)subview
{
   //we don't want to get NSViewFrameDidChangeNotifications from the subview once it is removed
   [[NSNotificationCenter defaultCenter] removeObserver:self
                   name:@"NSViewFrameDidChangeNotification"
                 object:subview];
   
   if([subview isKindOfClass:[SSEventView class]] && eventViewArray) {
      //remove this view from our eventViewArray
      //dealloc is called before subviews are removed when the chart window is closed
      //because of this a crash will happen on closing the window if we don't check
      //to see that eventViewArray exists
      [[NSNotificationCenter defaultCenter] removeObserver:self
                                                      name:@"SSActorChangedForView"
                                                    object:subview];

      [eventViewArray removeObjectIdenticalTo:subview];

      //get the dateDisplayView to remove tracking cell for this event
      [dateDisplayView removeTrackingCellForEvent:[(SSEventView *)subview owner]];
   } else {
      if([subview isKindOfClass:[SSCommentView class]] && commentViewArray) {
         [commentViewArray removeObjectIdenticalTo:subview];
      }
   }

   if(![subview isKindOfClass:[SSJumpMapView class]]) {
      //remove the view from the jumpmap
      if(theDocument) {
         [[theDocument jumpMapView] removeJumpMapRepForView:subview];
      }
   } else {
      //it is the jump map
      displayingJumpMap = NO;
   }
   
   [self invalidateSelection];
}

//*************************mouse events

- (void)mouseDown:(NSEvent *)theEvent
{
   NSPoint converted = [self convertPoint:[theEvent locationInWindow] fromView:nil];

   id objectAtPoint = [self nonViewObjectAtPoint:converted];
   
   if(objectAtPoint) { //there is an object under the click so work out what needs to be done
      if(([theEvent modifierFlags] & NSCommandKeyMask) || ([theEvent modifierFlags] & NSShiftKeyMask)) {
         if([objectAtPoint isSelected]) { //already selected we want to remove our selected status
            [self invalidateSelectionForObject:objectAtPoint];
         } else { //using command key they are selecting multiple objects
            [self markAsSelected:objectAtPoint byAddingToSelection:YES];
         }
      } else { //no command just select objectAtPoint
         [self markAsSelected:objectAtPoint byAddingToSelection:NO];
      }
      [self setNeedsDisplayInRect:[self visibleRect]];
   } else {
      //they clicked on empty space
      [self invalidateSelection];
   }

   anchorPoint = converted;
   mousePosition = converted;
}

- (void)mouseUp:(NSEvent *)theEvent
{
   [self selectObjectsWithinSelectionRect];
   [self setSelectionRect:NSZeroRect]; //get rid of the selection rectangle
   [selectionView setNeedsDisplay:YES];
}

- (void)mouseDragged:(NSEvent *)theEvent
{
   NSPoint converted = [self convertPoint:[theEvent locationInWindow] fromView:nil];
   float scrollX, scrollY;

   if(([theEvent modifierFlags] & NSControlKeyMask)) { //user wants to make a selection
      [self setSelectionRect:NSMakeRect( MIN(anchorPoint.x, converted.x),
                                         MIN(anchorPoint.y, converted.y),
                                         fabs(anchorPoint.x - converted.x),
                                         fabs(anchorPoint.y - converted.y))];

      [[self superview] autoscroll:theEvent];
   } else { //they are dragging the view
      if(converted.x < mousePosition.x) {
         //we are moving the view right
         scrollX = (NSMinX([self visibleRect]) + (mousePosition.x - converted.x));
         mousePosition.x = (converted.x + (mousePosition.x - converted.x));
      } else {
         if(converted.x > mousePosition.x) {
            //moving to the left
            scrollX = (NSMinX([self visibleRect]) - (converted.x - mousePosition.x));
            mousePosition.x = (converted.x - (converted.x - mousePosition.x));
         } else {
            //no movement on x
            scrollX = NSMinX([self visibleRect]);
            mousePosition.x = converted.x;
         }
      }

      if(converted.y < mousePosition.y) {
         //we are moving the view down
         scrollY = (NSMinY([self visibleRect]) + (mousePosition.y - converted.y));
         mousePosition.y = (converted.y + (mousePosition.y - converted.y));
      } else {
         if(converted.y > mousePosition.y) {
            //we are moving up
            scrollY = (NSMinY([self visibleRect]) - (converted.y - mousePosition.y));
            mousePosition.y = (converted.y - (converted.y - mousePosition.y));
         } else {
            //no movement on y
            scrollY = NSMinY([self visibleRect]);
            mousePosition.y = converted.y;
         }
      }

      [self scrollPoint:NSMakePoint(scrollX, scrollY)];
   }

   [selectionView setNeedsDisplay:YES];
}

//*************************
// so that we can get keyboard events
- (BOOL)acceptsFirstResponder {
   return YES;
}

- (void)keyDown:(NSEvent *)theEvent
{
   NSString *characters;
   unichar c;

   characters = [theEvent charactersIgnoringModifiers];
   c = [characters characterAtIndex:0];

   switch(c) {
      case NSUpArrowFunctionKey:
         if(([theEvent modifierFlags] & NSCommandKeyMask)) {
            //user has pressed the command key so do a super jump
            [self scrollPoint:NSMakePoint(NSMinX([self visibleRect]), (NSMinY([self visibleRect]) - (NSHeight([self visibleRect]) * .75)))];
         } else {
            [self scrollPoint:NSMakePoint(NSMinX([self visibleRect]), (NSMinY([self visibleRect]) - 30))];
         }

         break;
      case NSDownArrowFunctionKey:
         if(([theEvent modifierFlags] & NSCommandKeyMask)) {
            //user has pressed the command key so do a super jump
            [self scrollPoint:NSMakePoint(NSMinX([self visibleRect]), (NSMinY([self visibleRect]) + (NSHeight([self visibleRect]) * .75)))];
         } else {
            [self scrollPoint:NSMakePoint(NSMinX([self visibleRect]), (NSMinY([self visibleRect]) + 30))];
         }

         break;
      case NSLeftArrowFunctionKey:
         if(([theEvent modifierFlags] & NSCommandKeyMask)) {
            //user has pressed the command key so do a super jump
            [self scrollPoint:NSMakePoint(NSMinX([self visibleRect]) - (NSWidth([self visibleRect]) * .75), NSMinY([self visibleRect]))];
         } else {
            [self scrollPoint:NSMakePoint((NSMinX([self visibleRect]) - 30), NSMinY([self visibleRect]))];
         }

         break;
      case NSRightArrowFunctionKey:
         if(([theEvent modifierFlags] & NSCommandKeyMask)) {
            //user has pressed the command key so do a super jump
            [self scrollPoint:NSMakePoint(NSMinX([self visibleRect]) + (NSWidth([self visibleRect]) * .75), NSMinY([self visibleRect]))];
         } else {
            [self scrollPoint:NSMakePoint((NSMinX([self visibleRect]) + 30), NSMinY([self visibleRect]))];
         }

         break;
      default:
         break;
   }

   [super keyDown:theEvent];
}

- (IBAction)copy:(id)sender {
   if ([selectedObjects count] > 0) {
      NSPasteboard *pboard = [NSPasteboard generalPasteboard];
      
      [pboard declareTypes:[NSArray arrayWithObjects:SSCatalystChartObjectType, nil] owner:nil];
      [pboard setData:[self pasteboardDataForObjects:selectedObjects] forType:SSCatalystChartObjectType];
   }
}

- (IBAction)cut:(id)sender {
   [self copy:sender];
   int i = 0;
   NSMutableArray *deleteArray = [NSMutableArray array];
   
   while(i < [selectedObjects count]) {
      //add the objects that we are cutting to the delete array
      if([[selectedObjects objectAtIndex:i] isKindOfClass:[SSCommentView class]] ||
         [[selectedObjects objectAtIndex:i] isKindOfClass:[SSEventView class]]) {
         [deleteArray addObject:[selectedObjects objectAtIndex:i]];
      }
      i++;
   }
   
   i = 0;
   while(i < [deleteArray count]) {
      [theDocument deleteObject:[[deleteArray objectAtIndex:i] owner]];
      i++;
   }
   
   [self invalidateSelection];
}

- (IBAction)paste:(id)sender {
   NSPasteboard *pboard = [NSPasteboard generalPasteboard];
   NSString *type = [pboard availableTypeFromArray:[NSArray arrayWithObjects:SSCatalystChartObjectType, nil]];
   
   if (type) {
      NSLog(@"paste");
      if ([type isEqualToString:SSCatalystChartObjectType]) {
         NSString *string = [[NSString allocWithZone:[self zone]] initWithData:[pboard dataForType:type] encoding:NSASCIIStringEncoding];
         NSDictionary *dict = [string propertyList];
         NSMutableArray *objectArray = [dict objectForKey:@"objectArray"];
         int i = 0;
         while(i < [objectArray count]) {
            //loop and create the pasted objects
            if([[[objectArray objectAtIndex:i] objectForKey:@"objectType"] isEqualToString:@"SSEventObject"]) {
               //create a eb
               if([theDocument eventWithAddress:[[objectArray objectAtIndex:i] objectForKey:@"address"]]) {
                  //there is an EB with this address get a new one
                  [[objectArray objectAtIndex:i] setObject:[theDocument addressForNewEvent] forKey:@"address"];
               }
               
               [theDocument addEventToDocument:[[SSEventObject alloc] initWithPboardRepresentation:[objectArray objectAtIndex:i] 
                                                                                              xPos:([self visibleRect].origin.x + (i*10))]];
            } else if ([[[objectArray objectAtIndex:i] objectForKey:@"objectType"] isEqualToString:@"SSCatalystComment"]) {
               //create a comment
               [theDocument addCommentToDocument:[[SSCatalystComment alloc] initWithPboardRepresentation:[objectArray objectAtIndex:i] 
                                                                                                  origin:NSMakePoint(([self visibleRect].origin.x + (i*10)), [self visibleRect].origin.y)]];   
            }
            
            i++;
         }
      }
   }
}

- (NSData *)pasteboardDataForObjects:(NSArray *)array {
   NSMutableArray *copyArray = [NSMutableArray array];
   NSMutableDictionary *pboardDict = [NSMutableDictionary dictionary];
   int i = 0;
   
   while(i < [array count]) {
      if([[array objectAtIndex:i] isKindOfClass:[SSCommentView class]] ||
         [[array objectAtIndex:i] isKindOfClass:[SSEventView class]]) {
         [copyArray addObject:[[[array objectAtIndex:i] owner] pastboardRepresentation]];
      }
      i++;
   }
   
   [pboardDict setObject:copyArray forKey:@"objectArray"];

   return [[pboardDict description] dataUsingEncoding:NSASCIIStringEncoding];
}
//*************************

- (NSMutableArray *)eventViews
{
   return eventViewArray;
}

- (NSMutableArray *)commentViews
{
   return commentViewArray;
}

//*************************
- (void)viewScrolled
{
   if(displayingJumpMap) {
      [[theDocument jumpMapView] setFrameOrigin:NSMakePoint((NSMaxX([self visibleRect]) - NSWidth([[theDocument jumpMapView] frame])), (NSMaxY([self visibleRect]) - NSHeight([[theDocument jumpMapView] frame])))]; //move the jump map to the bottom right of the visible rect
   }

   [self setNeedsDisplayInRect:[self visibleRect]];
}

- (void)updateAfterFrameChange
{
   [selectionView setFrameSize:NSMakeSize(NSWidth([self bounds]), NSHeight([self bounds]))]; //grow or shrink the selection view

   if(displayingJumpMap) {
      [[theDocument jumpMapView] setFrameOrigin:NSMakePoint((NSMaxX([self visibleRect]) - NSWidth([[theDocument jumpMapView] frame])), (NSMaxY([self visibleRect]) - NSHeight([[theDocument jumpMapView] frame])))]; //move the jump map to the bottom right of the visible rect
   }
}

//*************************

- (void)resizeIfSubviewOutOfBounds:(NSNotification *)note
{
   if([self displayMode] == SSMatrixDisplayMode) {
      //resizes our frame if subview contained within note is beyond our bounds
      float endX = ([[note object] frame].origin.x + [[note object] frame].size.width);
      float endY = ([[note object] frame].origin.y + [[note object] frame].size.height);

      if(endX > [self frame].size.width) {
         [self setFrameSize:NSMakeSize(endX, [self frame].size.height)];
      }

      if(endY > [self frame].size.height) {
         [self setFrameSize:NSMakeSize([self frame].size.width, endY)];
      }

      //----------
      if([[self selectedObjects] containsObject:[note object]]) {
         //this view is selected so scroll
         [self scrollRectToVisible:[[note object] frame]];
      }
   }
}

int sortByXPosition(NSView *obj1, NSView *obj2, void *context )
{
   NSComparisonResult result;
   float pos = NSMaxX([obj1 frame]) - NSMaxX([obj2 frame]);

   if(pos < 0) {
      result = NSOrderedAscending;
   } else {
      if(pos > 0) {
         result = NSOrderedDescending;
      } else {
         result = NSOrderedSame;
      }
   }

   return result;
}

- (void)sizeToFit
{
   float maxX = 0;
   NSMutableArray *views = [NSMutableArray array];
   [views addObjectsFromArray:eventViewArray];
   [views addObjectsFromArray:commentViewArray]; //get an array with all of the comment and event views

   [views sortUsingFunction:sortByXPosition context:nil];

   maxX = NSMaxX([[views lastObject] frame]);

   //convert the max values
   maxX = (maxX / [self convertSize:NSMakeSize(1,1) fromView:nil].width);
   
   if(NSMaxX([self frame]) != maxX) {
      //the frame needs to change
      if(NSWidth([[self superview] frame]) > maxX) {
         maxX = NSWidth([[self superview] frame]);
      }

      [self setFrameSize:NSMakeSize(maxX, NSHeight([self frame]))];
   }
}

- (void)sizeToEndDate:(NSCalendarDate *)endDate
{
   float endX = [self xPositionForDate:endDate];

   if(endX > 1000000) {
      //it's more than 100000 so limit
      [self setFrameSize:NSMakeSize(1000000, NSHeight([self frame]))];

      //tell user
      NSRunAlertPanel(NSLocalizedString(@"Alert", nil), [NSString stringWithFormat:@"%@ %@.", NSLocalizedString(@"TooLargeDateRange", nil), [[self dateForXPosition:1000000 isEstimated:NO] descriptionWithCalendarFormat:@"%Y:%m:%d %H:%M:%S:%F"]], @"OK", nil, nil, nil);
      //use dateForXPosition
   } else {
      if(NSWidth([[self superview] frame]) > endX) {
         //end is smaller that allowd so grow it
         endX = NSWidth([[self superview] frame]);
      }
      
      [self setFrameSize:NSMakeSize(endX, NSHeight([self frame]))];
   }
   [self setNeedsDisplayInRect:[self visibleRect]];
}

//*************************

- (void)setLinkIfAboveAnotherEB:(SSEventView *)linkingView point:(NSPoint)point
{
   NSEnumerator *enumerator = [eventViewArray objectEnumerator];
   id anObject;

   while (anObject = [enumerator nextObject]) {
      //enumerate through all subviews if above a view which is not linkingView set the connection
      if(anObject != linkingView) {
         if([self pointIsWithinRect:point rect:[anObject frame]]) {
            //click was above this object make the connection
            [[linkingView owner] addLinkToObject:[anObject owner]];
            break;
         }
      }
   }
}

- (BOOL)pointIsWithinRect:(NSPoint)point rect:(NSRect)rect
{
   if(point.x > NSMinX(rect) && point.x < NSMaxX(rect)) {
      //it is within the x range of this rect
      if(point.y > NSMinY(rect) && point.y < NSMaxY(rect)) {
         //it is within this rect
         return YES;
      }
   }
   return NO;
}

- (void)prepareForReorderingFromActor:(NSNotification *)note
{
   //before an actor name is changed this method is run topIndexForRecalculation is used so that we only recalculate for rows below the old position rather than for all EBs
   topIndexForRecalculation = [[theDocument filteredActorArray] indexOfObject:[note object]];
}

- (void)recalculateEBPositions
{
   //set the y origin of all event views.
   NSMutableDictionary *positionDictionary = [[NSMutableDictionary alloc] init];
   NSEnumerator *enumerator = [eventViewArray objectEnumerator];
   id anObject;
   int actorIndex;
   NSNumber *yPos;
   float rowHeight = [theDocument tableViewRowHeight];
   NSPoint origin;

   while(anObject = [enumerator nextObject]) {
      yPos = [positionDictionary objectForKey:[[anObject owner] actor]];

      if(!yPos) {
         //we don't have a position for this actor so create one
         actorIndex = [[theDocument filteredActorArray] indexOfObject:[[anObject owner] actor]];
         yPos = [NSNumber numberWithFloat:((rowHeight * actorIndex) + SSFirstEBYPosition)];
         [positionDictionary setObject:yPos forKey:[[anObject owner] actor]]; //add this position to the dictionary
      }

      origin = NSMakePoint(NSMinX([anObject frame]), [yPos floatValue]);
      [anObject setFrameOrigin:origin];
   }

   [self setNeedsDisplayInRect:[self visibleRect]];
   [positionDictionary release];
}

- (void)recalculateEBPositionsForViewAndBelow:(NSNotification *)note
{
   //set the y origin of all event views. Called after user drags table view rows
   NSMutableDictionary *positionDictionary = [[NSMutableDictionary alloc] init];
   NSEnumerator *enumerator = [eventViewArray objectEnumerator];
   id anObject;
   int actorIndex;
   NSNumber *yPos;
   float rowHeight = [theDocument tableViewRowHeight];
   NSPoint origin;

   if(topIndexForRecalculation == -5) {
      //we only want to set the position of the view contained in note
      actorIndex = [[theDocument filteredActorArray] indexOfObject:[[[note object] owner] actor]];
      yPos = [NSNumber numberWithFloat:((rowHeight * actorIndex) + SSFirstEBYPosition)];
      origin = NSMakePoint(NSMinX([[note object] frame]), [yPos floatValue]);

      [[note object] setFrameOrigin:origin];
   } else {
      //recalculate positions for all below topIndexForRecalculation
      while(anObject = [enumerator nextObject]) {
         if([[theDocument filteredActorArray] indexOfObject:[[anObject owner] actor]] >= topIndexForRecalculation || [anObject isEqualTo:[note object]]) {
            yPos = [positionDictionary objectForKey:[[anObject owner] actor]];

            if(!yPos) {
               //we don't have a position for this actor so create one
               actorIndex = [[theDocument filteredActorArray] indexOfObject:[[anObject owner] actor]];
               yPos = [NSNumber numberWithFloat:((rowHeight * actorIndex) + SSFirstEBYPosition)];
               [positionDictionary setObject:yPos forKey:[[anObject owner] actor]]; //add this position to the dictionary
            }

            origin = NSMakePoint(NSMinX([anObject frame]), [yPos floatValue]);

            [anObject setFrameOrigin:origin];
         }
      }
   }

   [self setNeedsDisplayInRect:[self visibleRect]];
   [positionDictionary release];
}

//*************************
- (void)setTemporaryLinkWithStartPoint:(NSPoint)start endPoint:(NSPoint)end
{
   [temporaryLink removeAllPoints];
   [temporaryLink moveToPoint:start];
   [temporaryLink lineToPoint:end];
   [self setNeedsDisplay:YES];
}

- (void)removeTemporaryLinkPoints
{
   [temporaryLink removeAllPoints];
   [self setNeedsDisplay:YES];
}

//*************************subview selection
- (void)invalidateSelection
{
   if(selectedObjects) {
      [selectedObjects makeObjectsPerformSelector:@selector(setUnselected)];

      [selectedObjects removeAllObjects];

      //post a notification so that the inspector will update
      [[NSNotificationCenter defaultCenter] postNotificationName:@"SSSelectionChanged" object:self];
   }
}

- (void)invalidateSelectionForObject:(id)object
{
   [selectedObjects removeObjectIdenticalTo:object];

   [object setUnselected];

   //post a notification so that the inspector will update
   [[NSNotificationCenter defaultCenter] postNotificationName:@"SSSelectionChanged" object:self];
}


- (void)markAsSelected:(id)s byAddingToSelection:(BOOL)add
{
   if(![s isSelected]) { //if it is already selected do nothing
      if(add) {
         [selectedObjects addObject:s];
      } else {
         [selectedObjects makeObjectsPerformSelector:@selector(setUnselected)]; //deselect the selected views
         [selectedObjects removeAllObjects];
         [selectedObjects addObject:s];
      }

      [s setSelected]; //tell it that it's selected

      //post a notification so that the inspector will update
      [[NSNotificationCenter defaultCenter] postNotificationName:@"SSSelectionChanged" object:self];
   }
}

- (void)selectObjectsWithinSelectionRect
{
   NSMutableArray *selectableViews = [NSMutableArray array];
   [selectableViews addObjectsFromArray:eventViewArray];
   [selectableViews addObjectsFromArray:commentViewArray]; //get an array with all selectable views

   NSEnumerator *viewEnumerator = [selectableViews objectEnumerator];
   id viewObject;
   NSEnumerator *linkEnumerator;
   id linkObject;
   NSEnumerator *diamondEnumerator = [[theDocument needsDiamondArray] objectEnumerator];
   id diamondObject;

   while (viewObject = [viewEnumerator nextObject]) {
      if([viewObject isFullyWithinRect:selectionRect]) {
         //this view is under the selection rect so mark it as selected
         [self markAsSelected:viewObject byAddingToSelection:YES];
      }

      //see if any of this Events links are within the selection rect
      if([viewObject isKindOfClass:[SSEventView class]]) {
         linkEnumerator = [[[viewObject owner] linkArray] objectEnumerator];
         while (linkObject = [linkEnumerator nextObject]) {
            if([linkObject isFullyWithinRect:selectionRect]) {
               //this link is under the selection rect so mark it as selected
               [self markAsSelected:linkObject byAddingToSelection:YES];
            }
         }
      }
   }

   while (diamondObject = [diamondEnumerator nextObject]) {
      if([diamondObject isFullyWithinRect:selectionRect]) {
         //this diamond is under the selection rect so mark it as selected
         [self markAsSelected:diamondObject byAddingToSelection:YES];
      }
   }

   //post a notification so that the inspector will update
   [[NSNotificationCenter defaultCenter] postNotificationName:@"SSSelectionChanged" object:self];
}

- (void)setSelectionRect:(NSRect)rect
{
   selectionRect = rect;
}

- (NSRect)selectionRect
{
   return selectionRect;
}

- (void)setSelectedViewsDistanceFromOrigin:(NSPoint)point
{
   NSEnumerator *enumerator = [selectedObjects objectEnumerator];
   id anObject;

   while (anObject = [enumerator nextObject]) {
      if([anObject respondsToSelector:@selector(setDistanceFromOrigin:)]) {
         [anObject setDistanceFromOrigin:point];
      }
   }
}

- (void)setSelectedViewsOriginWithPoint:(NSPoint)point
{
   NSEnumerator *enumerator = [selectedObjects objectEnumerator];
   id anObject;

   while (anObject = [enumerator nextObject]) {
      if([anObject respondsToSelector:@selector(setNewOriginWithPoint:)]) {
         [anObject setNewOriginWithPoint:point];

         if(NSMaxX([anObject frame]) > NSMaxX([self frame])) {
            //grow our frame to fit
            [self setFrameSize:NSMakeSize(NSMaxX([anObject frame]), NSHeight([self frame]))];
         }

         [self scrollRectToVisible:[anObject frame]];
      }
   }

   if([self displayMode] == SSMatrixDisplayMode) {
      [self sizeToFit]; //grow or shrink to fit all of the subviews
   }
}

- (NSMutableArray *)selectedObjects
{
   return selectedObjects;
}

- (BOOL)hasCopyableItemsSelected
{
   int i = 0;
   while(i < [selectedObjects count]) {
      if([[selectedObjects objectAtIndex:i] isKindOfClass:[SSCommentView class]] ||
         [[selectedObjects objectAtIndex:i] isKindOfClass:[SSEventView class]]) {
         return YES;
      }
      i++;
   }
   
   //no comments selected
   return NO;
}

- (void)selectAndDisplayObject:(id)object
{
   //select the object and scroll it to visible
   if([object respondsToSelector:@selector(setSelected)] && [object respondsToSelector:@selector(frame)]) {
      NSPoint point = NSMakePoint(NSMinX([object frame]), NSMinY([object frame]));
      [self markAsSelected:object byAddingToSelection:NO];
      [self scrollPoint:NSMakePoint(point.x, point.y)];
   }
}


- (void)setSelectedEventDraggingMode:(int)mode
{
   //set the dragging mode of the selected events
   int i = 0;

   while(i < [[self selectedObjects] count]) {
      if([[[self selectedObjects] objectAtIndex:i] respondsToSelector:@selector(setDraggingMode:)]) {
         [[[self selectedObjects] objectAtIndex:i] setDraggingMode:mode];
      }
      i++;
   }
}

- (void)updateSelectedEventDataAfterDrag
{
   //get the event views to update their dates
   int i = 0;

   while(i < [[self selectedObjects] count]) {
      if([[[self selectedObjects] objectAtIndex:i] respondsToSelector:@selector(updateDatesAfterDrag)]) {
         [(SSEventView *)[[self selectedObjects] objectAtIndex:i] updateDatesAfterDrag];
      }
      i++;
   }
}

//*************************

- (void)setTheDocument:(id)doc
{
   theDocument = doc;
}

//*************************
- (SSChartDate *)overlapViewFromDate
{
   return overlapViewFromDate;
}

- (void)setOverlapViewFromDate:(SSChartDate *)date
{
   //used to change the display date while in overlap
   [date retain];
   [overlapViewFromDate release];
   overlapViewFromDate = date;

   [self updateOverlapDisplayData];
   [[self eventViews] makeObjectsPerformSelector:@selector(updateAfterOverlapViewChange)];
}

- (void)setOverlapViewFromDate
{
   //called when view is switched to overlap
   [overlapViewFromDate release];
   overlapViewFromDate = [[theDocument overlapViewFromDate] retain];

   [self updateOverlapDisplayData];
}

//*************************

- (int)displayMode
{
   return ssDisplayMode;
}

- (void)setDisplayMode:(int)mode
{
   int i = 0;
   
   ssDisplayMode = mode;

   if(ssDisplayMode == SSOverlapDisplayMode) {
      //switching to overlap so need a view from date
      [self setOverlapViewFromDate];
      [self sizeToEndDate:[theDocument overlapViewToDate]];
   }

   //set the date display views mode
   [(SSDateDisplayView *)dateDisplayView setDisplayMode:mode];
   
   //set our events display mode
   while(i < [[self eventViews] count]) {
      [(SSEventView *)[[self eventViews] objectAtIndex:i] setDisplayMode:mode];
      i++;
   }

   [self recalculateEBPositions];

   if([self displayMode] == SSMatrixDisplayMode) {
      [self sizeToFit];
   }
}

//*************************
- (float)xPositionForDate:(NSCalendarDate *)date
{
   SSChartDate *viewFrom = [self overlapViewFromDate];

   return ([date timeIntervalSinceDate:viewFrom] / ([self overlapUnit] / [self overlapPixelsPerUnit]));
}

- (SSChartDate *)dateForXPosition:(float)xPos isEstimated:(BOOL)est
{
   //create a date based on our view from date and the xPosition on the chart
   SSChartDate *date = [[[SSChartDate alloc] initWithTimeInterval:(xPos * ([self overlapUnit] / [self overlapPixelsPerUnit]))
                                                        sinceDate:[self overlapViewFromDate]] autorelease];

   [date setIsEstimated:est];
   
   return date;
}

- (void)datesForFrame:(NSRect)rect begin:(SSChartDate **)begin end:(SSChartDate **)end beginIsEstimated:(BOOL)beginEst endIsEstimated:(BOOL)endEst
{
   *begin = [[[SSChartDate alloc] initWithTimeInterval:(NSMinX(rect) * ([self overlapUnit] / [self overlapPixelsPerUnit]))
                                            sinceDate:[self overlapViewFromDate]] autorelease];

   [*begin setIsEstimated:beginEst];
   
   *end = [[[SSChartDate alloc] initWithTimeInterval:(NSMaxX(rect) * ([self overlapUnit] / [self overlapPixelsPerUnit]))
                                             sinceDate:[self overlapViewFromDate]] autorelease];
   [*end setIsEstimated:endEst];
}

- (NSRect)frameForDates:(SSChartDate *)begin end:(SSChartDate *)end oldFrame:(NSRect)oldFrame
{
   SSChartDate *viewFrom = [self overlapViewFromDate];

   float minX = ([begin timeIntervalSinceDate:viewFrom] / ([self overlapUnit] / [self overlapPixelsPerUnit]));
   float maxX = ([end timeIntervalSinceDate:viewFrom] / ([self overlapUnit] / [self overlapPixelsPerUnit]));

   return NSMakeRect(minX, NSMinY(oldFrame), (maxX - minX), NSHeight(oldFrame));
}


//*************************

- (BOOL)isFlipped
{
   //changes the coordinate system so 0,0 is in the top left
   return YES;
}

- (BOOL)isOpaque
{
   // State that the view is opaque for optimization purposes.
   return YES;
}
//*************************

- (id)nonViewObjectAtPoint:(NSPoint)point
{
   NSEnumerator *diamondEnumerator = [[theDocument needsDiamondArray] objectEnumerator];
   id diamondObject;
   NSEnumerator *eventViewEnumerator = [[self eventViews] objectEnumerator];
   id eventViewObject;
   NSEnumerator *linkEnumerator;
   id linkObject;

   //see if we are clicking on a diamond
   while (diamondObject = [diamondEnumerator nextObject]) {
      if([diamondObject containsPoint:point]) {
         return diamondObject;
      }
   }

   //there's no diamond below the mouse click, see if there's a link
   NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
   while (eventViewObject = [eventViewEnumerator nextObject]) {
      linkEnumerator = [[[eventViewObject owner] linkArray] objectEnumerator];
      while (linkObject = [linkEnumerator nextObject]) {
         if([linkObject containsPoint:point]) {
            return linkObject;
         }
      }
   }
   [pool release];

   return nil; //no non view objects at point
}



//*************************
- (float)widthOfDateDisplayCells
{
   return NSWidth([dateDisplayView frameForCellAtDateIndex:0]);
}

//*************************


- (void)scaleUnitSquareToSize:(NSSize)newUnitSize
{   
   [super scaleUnitSquareToSize:newUnitSize];
   
   if([[self subviews] containsObject:[theDocument jumpMapView]]) {
      [[theDocument jumpMapView] setFrameSize:NSMakeSize((NSWidth([[theDocument jumpMapView] frame]) / newUnitSize.width),
                                                         (NSHeight([[theDocument jumpMapView] frame]) / newUnitSize.height))];
      [[theDocument jumpMapView] setFrameOrigin:NSMakePoint((NSMaxX([self visibleRect]) - NSWidth([[theDocument jumpMapView] frame])), (NSMaxY([self visibleRect]) - NSHeight([[theDocument jumpMapView] frame])))]; //move the jump map to the bottom right of the visible rect
   }

   if([self displayMode] == SSOverlapDisplayMode) {
      [self updateOverlapDisplayData];
   } else {
      [self sizeToFit];
   }
   [self setNeedsDisplayInRect:[self visibleRect]];
}

- (void)zoomOverlapIn
{
   [self setOverlapPixelsPerUnit:[self overlapPixelsPerUnit] + SSOverlapPixelsForZoom];

   [self updateOverlapDisplayData];
   [[self eventViews] makeObjectsPerformSelector:@selector(updateAfterOverlapViewChange)];
   [self sizeToEndDate:[theDocument overlapViewToDate]];
}

- (void)zoomOverlapOut
{
   [self setOverlapPixelsPerUnit:[self overlapPixelsPerUnit] - SSOverlapPixelsForZoom];

   [self updateOverlapDisplayData];
   [[self eventViews] makeObjectsPerformSelector:@selector(updateAfterOverlapViewChange)];
   [self sizeToEndDate:[theDocument overlapViewToDate]];
}

- (float)overlapUnit
{
   return overlapUnit;
}

- (void)setOverlapUnit:(float)unit
{
   overlapUnit = unit;

   if([self displayMode] == SSOverlapDisplayMode) {
      [self updateOverlapDisplayData];
      [[self eventViews] makeObjectsPerformSelector:@selector(updateAfterOverlapViewChange)];
      [self sizeToEndDate:[theDocument overlapViewToDate]];
   }
}

- (float)overlapPixelsPerUnit
{
   return overlapPixelsPerUnit;
}

- (void)setOverlapPixelsPerUnit:(float)pix
{
   overlapPixelsPerUnit = pix;

   if([self displayMode] == SSOverlapDisplayMode) {
      [self updateOverlapDisplayData];
      [[self eventViews] makeObjectsPerformSelector:@selector(updateAfterOverlapViewChange)];
      [self sizeToEndDate:[theDocument overlapViewToDate]];
   }
}

- (BOOL)validateMenuItem:(NSMenuItem *)item {
   SEL action = [item action];
   if (action == @selector(copy:) || action == @selector(cut:)) {
      if([self hasCopyableItemsSelected]) {
         return YES;
      } else {
         return NO;  
      }
   } else if (action == @selector(paste:)) {
      if([[NSPasteboard generalPasteboard] availableTypeFromArray:[NSArray arrayWithObjects:SSCatalystChartObjectType, nil]]) {
         return YES;
      } else {
         return NO;
      }
   }
   
   return YES;
}

//*************************

- (void)dealloc
{
   [[NSNotificationCenter defaultCenter] removeObserver:self];

   [temporaryLink release];
   [eventViewArray release];
   [commentViewArray release];
   [selectedObjects release];
   [overlapViewFromDate release];
   [overlapDisplayGenerator release];
   
   eventViewArray = nil;
   selectedObjects = nil;
   commentViewArray = nil;
   
   [super dealloc];
}


@end
